#include "snap/positionsnapper.h"
#include "view/view.h"
#include "view/scene.h"
#include "item/item.h"
#include "grid/grid.h"

#include <QAction>

#include <limits>

namespace SymbolEditor
{

    /*******************************************************************************
     * Snap Strategy
     *******************************************************************************/

    AbstractSnapStrategy::AbstractSnapStrategy(QObject *parent):
        m_view(parent),
        m_action(new QAction(nullptr))
    {

    }

    AbstractSnapStrategy::~AbstractSnapStrategy()
    {
    }

    QString AbstractSnapStrategy::name() const
    {
        return m_name;
    }

    QString AbstractSnapStrategy::group() const
    {
        return m_group;
    }

    void AbstractSnapStrategy::setGroup(const QString &name)
    {
        m_group = name;
    }

    void AbstractSnapStrategy::setLabel(const QString &label)
    {
        m_label = label;
    }

    void AbstractSnapStrategy::setName(const QString &name)
    {
        m_name = name;
    }

    void AbstractSnapStrategy::setShortcut(const QKeySequence &shortcut)
    {
        m_shortcut = shortcut;
    }

    void AbstractSnapStrategy::setIcon(const QIcon &icon)
    {
        m_icon = icon;
    }

    void AbstractSnapStrategy::setSnappedPosition(QPointF pos)
    {
        m_snappedPosition = pos;
    }

    void AbstractSnapStrategy::setSnappedItems(const QList<GraphicsItem *> &items)
    {
        m_snappedItems = items;
    }

    // TODO: don't include the item beeing edited/created
    QList<GraphicsItem *> AbstractSnapStrategy::itemsNearby(QPointF pos, qreal maxDistance)
    {
        QPainterPath region;
        region.addEllipse(pos, maxDistance, maxDistance);
        QList<QGraphicsItem *> graphicsItems = view()->scene()->items(region, Qt::IntersectsItemShape);
        QList<GraphicsItem *> schItems;
        for (QGraphicsItem *graphicsItem : graphicsItems)
        {
            // TODO: use type()
            GraphicsItem *schItem = dynamic_cast<GraphicsItem *>(graphicsItem);
            if (schItem != nullptr)
            {
                schItems << schItem;
            }
        }
        return schItems;
    }

    QPair<GraphicsItem *, QPointF> AbstractSnapStrategy::closestItemPoint(QPointF pos,
                                                          const QMultiMap<GraphicsItem *, QPointF> &candidates)
    {
        QPointF closestPoint;
        GraphicsItem *closestItem = nullptr;
        qreal minDistance = std::numeric_limits<qreal>::max();
        for (GraphicsItem *item : candidates.keys())
        {
            for (QPointF point : candidates.values(item))
            {
                qreal distance = (point - pos).manhattanLength();
                if (distance < minDistance)
                {
                    closestItem = item;
                    closestPoint = point;
                    minDistance = distance;
                }
            }
        }
        return qMakePair<GraphicsItem *, QPointF>(closestItem, closestPoint);
    }

    void AbstractSnapStrategy::updateAction()
    {
        m_action->setShortcut(m_shortcut);
        m_action->setToolTip(QString("%1 <i>%2</i>").arg(m_label).arg(m_shortcut.toString()));
        m_action->setIcon(m_icon);
        m_action->setCheckable(true);
        m_action->setChecked(false);
    }

    QGraphicsScene *SymbolEditor::AbstractSnapStrategy::scene() const
    {
        return m_scene;
    }

    void SymbolEditor::AbstractSnapStrategy::setScene(QGraphicsScene *scene)
    {
        if (m_scene != nullptr)
        {
            m_scene->removeEventFilter(this);
        }

        m_scene = scene;

        if (m_scene != nullptr)
        {
            m_scene->installEventFilter(this);
        }
    }

    QAction *AbstractSnapStrategy::action() const
    {
        return m_action;
    }

    QPainterPath AbstractSnapStrategy::decoration() const
    {
        QPainterPath decoration;
        //decoration.addEllipse(QPoint(0, 0), 2, 2);
        decoration.addText(QPoint(10, 25), QFont(), name());
        return decoration;
    }

    bool AbstractSnapStrategy::snap(QPointF mousePos, qreal maxDistance)
    {
        Q_UNUSED(mousePos);
        Q_UNUSED(maxDistance);
        return false;
    }

    QList<GraphicsItem *> AbstractSnapStrategy::snappedItems() const
    {
        return m_snappedItems;
    }

    QPointF AbstractSnapStrategy::snappedPosition() const
    {
        return m_snappedPosition;
    }

    bool AbstractSnapStrategy::isEnabled() const
    {
        return m_action->isChecked();
    }

    View *AbstractSnapStrategy::view()
    {
        return m_view;
    }

    /*******************************************************************************
    * No Snap Strategy (Free)
    *******************************************************************************/

    NoSnapStrategy::NoSnapStrategy(View *view):
        AbstractSnapStrategy(view)
    {
        setLabel("No <b>S</b>nap (<b>F</b>ree)");
        setName("Free");
        setGroup("leda.snap.default");
        setShortcut(QKeySequence("s,f"));
        setIcon(QIcon(":/icons/snap/snap-free.svg"));
        updateAction();
    }

    bool NoSnapStrategy::snap(QPointF mousePos, qreal maxDistance)
    {
        Q_UNUSED(maxDistance);
        setSnappedPosition(mousePos);
        return true;
    }

    /*******************************************************************************
    * Snap To Grid Strategy
    *******************************************************************************/

    SnapToGridStrategy::SnapToGridStrategy(View *view):
        AbstractSnapStrategy(view)
    {
        setLabel("<b>S</b>nap to <b>G</b>rid");
        setName("Grid");
        setGroup("leda.snap.default");
        setShortcut(QKeySequence("s,g"));
        setIcon(QIcon(":/icons/snap/snap-grid.svg"));
        updateAction();
    }

    bool SnapToGridStrategy::snap(QPointF mousePos, qreal maxDistance)
    {
        Q_UNUSED(maxDistance);
        // TODO: for (GraphicsGrid *grid: view().grids()) { ... }
        // TODO: rename GraphicsGrid::snap() ?
        // TODO: or use the grid manager?
        const Grid *grid = view()->grid();
        QPointF pos = grid->snap(view()->pixelSize(), mousePos);
        setSnappedPosition(pos);
        return true;
    }

    /*******************************************************************************
    * Snap To GraphicsItem's Hot Spots Strategy
    *******************************************************************************/

    // TBD: snap to handles (hot spots), vs snap to ref points
    SnapToItemHotSpotsStrategy::SnapToItemHotSpotsStrategy(View *view):
        AbstractSnapStrategy(view)
    {
        setLabel("<b>S</b>nap to <b>R</b>eferences");
        setName("Reference");
        setGroup("leda.snap.default");
        setShortcut(QKeySequence("s,r"));
        setIcon(QIcon(":/icons/snap/snap-reference.svg"));
        updateAction();
    }

    bool SnapToItemHotSpotsStrategy::snap(QPointF mousePos, qreal maxDistance)
    {
        QMultiMap<GraphicsItem *, QPointF> candidates;
        QPair<GraphicsItem *, QPointF> winner;

        QList<GraphicsItem *> items = itemsNearby(mousePos, maxDistance);
        for (GraphicsItem *item : items)
        {
            for (QPointF hotSpot : item->hotSpots())
            {
                QPointF pos = item->mapToScene(hotSpot);
                candidates.insert(item, pos);
            }
        }

        if (candidates.isEmpty())
        {
            return false;
        }

        winner = closestItemPoint(mousePos, candidates);
        setSnappedItems(QList<GraphicsItem *>() << winner.first);
        setSnappedPosition(winner.second);
        return true;
    }

    /*******************************************************************************
    * Snap To GraphicsItem's End Points Strategy
    *******************************************************************************/

    SnapToItemEndPointStrategy::SnapToItemEndPointStrategy(View *view):
        AbstractSnapStrategy(view)
    {
        setLabel("<b>S</b>nap to <b>E</b>nd-points");
        setName("End point");
        setGroup("leda.snap.default");
        setShortcut(QKeySequence("s,e"));
        setIcon(QIcon(":/icons/snap/snap-end-point.svg"));
        updateAction();
    }

    bool SnapToItemEndPointStrategy::snap(QPointF mousePos, qreal maxDistance)
    {
        QMultiMap<GraphicsItem *, QPointF> candidates;
        QPair<GraphicsItem *, QPointF> winner;

        QList<GraphicsItem *> items = itemsNearby(mousePos, maxDistance);
        for (GraphicsItem *item : items)
        {
            for (QPointF hotSpot : item->endPoints())
            {
                QPointF pos = item->mapToScene(hotSpot);
                candidates.insert(item, pos);
            }
        }

        if (candidates.isEmpty())
        {
            return false;
        }

        winner = closestItemPoint(mousePos, candidates);
        setSnappedItems(QList<GraphicsItem *>() << winner.first);
        setSnappedPosition(winner.second);
        return true;
    }

    /*******************************************************************************
    * Snap To GraphicsItem's Mid Points Strategy
    *******************************************************************************/

    SnapToItemMidPointStrategy::SnapToItemMidPointStrategy(View *view):
        AbstractSnapStrategy(view)
    {
        setLabel("<b>S</b>nap to <b>M</b>id-points");
        setName("Mid point");
        setGroup("leda.snap.default");
        setShortcut(QKeySequence("s,m"));
        setIcon(QIcon(":/icons/snap/snap-mid-point.svg"));
        updateAction();
    }

    bool SnapToItemMidPointStrategy::snap(QPointF mousePos, qreal maxDistance)
    {
        QMultiMap<GraphicsItem *, QPointF> candidates;
        QPair<GraphicsItem *, QPointF> winner;

        QList<GraphicsItem *> items = itemsNearby(mousePos, maxDistance);
        for (GraphicsItem *item : items)
        {
            for (QPointF hotSpot : item->midPoints())
            {
                QPointF pos = item->mapToScene(hotSpot);
                candidates.insert(item, pos);
            }
        }

        if (candidates.isEmpty())
        {
            return false;
        }

        winner = closestItemPoint(mousePos, candidates);
        setSnappedItems(QList<GraphicsItem *>() << winner.first);
        setSnappedPosition(winner.second);
        return true;
    }

    /*******************************************************************************
    * Snap To GraphicsItem's Shape Points Strategy
    *******************************************************************************/

    SnapToItemShapeStrategy::SnapToItemShapeStrategy(View *view):
        AbstractSnapStrategy(view)
    {
        setLabel("<b>S</b>nap to <b>S</b>hapes");
        setName("Shape point");
        setGroup("leda.snap.default");
        setShortcut(QKeySequence("s,s"));
        setIcon(QIcon(":/icons/snap/snap-shape.svg"));
        updateAction();
    }

    bool SnapToItemShapeStrategy::snap(QPointF mousePos, qreal maxDistance)
    {
        QMultiMap<GraphicsItem *, QPointF> candidates;
        QPair<GraphicsItem *, QPointF> winner;

        QList<GraphicsItem *> items = itemsNearby(mousePos, maxDistance);
        for (GraphicsItem *item : items)
        {
            QPointF itemPos = item->mapFromScene(mousePos);
            for (QPointF itemPoint : item->nearestPoints(itemPos))
            {
                QPointF pos = item->mapToScene(itemPoint);
                candidates.insert(item, pos);
            }
        }

        if (candidates.isEmpty())
        {
            return false;
        }

        winner = closestItemPoint(mousePos, candidates);
        setSnappedItems(QList<GraphicsItem *>() << winner.first);
        setSnappedPosition(winner.second);
        return true;
    }

    /*******************************************************************************
    * Snap To GraphicsItem's Center Points Strategy
    *******************************************************************************/

    SnapToItemCenterStrategy::SnapToItemCenterStrategy(View *view):
        AbstractSnapStrategy(view)
    {
        setLabel("<b>S</b>nap to <b>C</b>enter-points");
        setName("Center point");
        setGroup("leda.snap.default");
        setShortcut(QKeySequence("s,c"));
        setIcon(QIcon(":/icons/snap/snap-center.svg"));
        updateAction();
    }

    bool SnapToItemCenterStrategy::snap(QPointF mousePos, qreal maxDistance)
    {
        QMultiMap<GraphicsItem *, QPointF> candidates;
        QPair<GraphicsItem *, QPointF> winner;

        QList<GraphicsItem *> items = itemsNearby(mousePos, maxDistance);
        for (GraphicsItem *item : items)
        {
            for (QPointF center : item->centerPoints())
            {
                QPointF pos = item->mapToScene(center);
                candidates.insert(item, pos);
            }
        }

        if (candidates.isEmpty())
        {
            return false;
        }

        winner = closestItemPoint(mousePos, candidates);
        setSnappedItems(QList<GraphicsItem *>() << winner.first);
        setSnappedPosition(winner.second);
        return true;
    }

    /*******************************************************************************
    * Snap Manager
    *******************************************************************************/

    // TODO: Add auto strategy:
    //Snaps to all end points, intersection points, middle points,
    //reference points and grid points in this order of priority.

    SnapManager::SnapManager(View *view):
        QObject(view)
    {
        m_defaultStrategy = new NoSnapStrategy(view);
        m_strategies << m_defaultStrategy
                     << new SnapToGridStrategy(view)
                     << new SnapToItemHotSpotsStrategy(view)
                     << new SnapToItemEndPointStrategy(view)
                     << new SnapToItemMidPointStrategy(view)
                     << new SnapToItemCenterStrategy(view)
                     << new SnapToItemShapeStrategy(view);

        for (const QString &group : groups())
        {
            QActionGroup *actionGroup = new QActionGroup(this);
            actionGroup->setExclusive(true);
            for (QAction *action : actions(group))
            {
                action->setActionGroup(actionGroup);
            }
        }
        m_defaultStrategy->action()->setChecked(true);
        m_winnerStrategy = m_defaultStrategy;
    }

    SnapManager::~SnapManager()
    {
        qDeleteAll(m_strategies);
    }

    QList<QString> SnapManager::groups() const
    {
        QList<QString> result;
        for (AbstractSnapStrategy *strategy : m_strategies)
        {
            if (!result.contains(strategy->group()))
            {
                result.append(strategy->group());
            }
        }
        return result;
    }

    QList<QAction *> SnapManager::actions() const
    {
        QList<QAction *> result;
        for (AbstractSnapStrategy *strategy : m_strategies)
        {
            if (group.isEmpty() || strategy->group() == group)
            {
                result.append(strategy->action());
            }
        }
        return result;
    }

    QPainterPath SnapManager::decoration() const
    {
        Q_ASSERT(m_winnerStrategy != nullptr);
        if (m_winnerStrategy != m_defaultStrategy)
        {
            return m_winnerStrategy->decoration();
        }
        else
        {
            return QPainterPath();    // Or make NoSnapStrategy returns an empty decoration?
        }
    }

    bool SnapManager::snap(QPointF mousePos, qreal maxDistance)
    {
        m_winnerStrategy = nullptr;
        qreal minDistance = std::numeric_limits<qreal>::max();
        for (AbstractSnapStrategy *strategy : m_strategies)
        {
            if (!strategy->isEnabled())
            {
                continue;
            }
            if (!strategy->snap(mousePos, maxDistance))
            {
                continue;
            }
            qreal distance = (strategy->snappedPosition() - mousePos).manhattanLength();
            if (distance < minDistance)
            {
                m_winnerStrategy = strategy;
                minDistance = distance;
            }
        }
        return m_winnerStrategy != nullptr;
    }

    QList<GraphicsItem *> SnapManager::snappedItems() const
    {
        Q_ASSERT(m_winnerStrategy != nullptr);
        return m_winnerStrategy->snappedItems();
    }

    QPointF SnapManager::snappedPosition() const
    {
        Q_ASSERT(m_winnerStrategy != nullptr);
        return m_winnerStrategy->snappedPosition();
    }

    AbstractSnapStrategy *SnapManager::snappingStrategy() const
    {
        return m_winnerStrategy;
    }

    void SnapManager::setScene(Scene *scene)
    {
        Q_UNUSED(scene);
    }

    void SnapManager::applySettings(const Settings &settings)
    {
        Q_UNUSED(settings);
    }

}
